"""
Tool-specific initialisation for EiffelStudio.
This does not work with EiffelStudio 5.6 or earlier.
"""

import os, glob, sys, shutil, datetime, subprocess, re, xml.dom.minidom
from SCons.Script import *
	
log_file = None

def log_open(env):
	global log_file

	if env['ECLOG'] == '':
		log_file = sys.stdout
	elif log_file == None:
		log_file = open(env['ECLOG'], 'w+')
	elif log_file.closed:
		log_file = open(env['ECLOG'], 'a+')

def log(s):
	log_file.write(str(s) + '\n')

def log_date():
	log(datetime.datetime.now())

def log_process(args):
	commandline = subprocess.list2cmdline(args)
	if log_file != sys.stdout: print ('  ' + commandline)
	log(commandline)
	log_file.flush()
	subprocess.call(args, stdout = log_file, stderr = subprocess.STDOUT)

def log_file_tail():
	"""The last thousand characters of the log file."""
	result = ''

	if log_file != sys.stdout:
		if log_file.tell() > 1000:
			log_file.seek(-1000, 1)
		else:
			log_file.seek(0)

		result = '... ' + log_file.read()
		log_file.seek(0, 2)

	return result

def ec_action(target, source, env):
	"""
	The Eiffel Builder's action function, running the Eiffel compiler.
	Parameters are as returned by ec_emitter():
	 * target: the paths to the files to be built, as generated by ec_emitter().
	 * source[0]: the ECF file.
	 * source[1], source[2], etc.: any additional dependencies.
	 * env['ECLOG']: name of file to which all compiler output is logged (stdout if empty).
	 * env['ECFLAGS']: Eiffel compiler flags: -finalize, -freeze, -clean, -project_path, -target, etc.
	Result is 0 (success) if all targets are built; else 1.
	(Note that the Eiffel compiler's return code is unreliable: it returns 0 if C compilation fails.)
	"""
	result = 0

	log_open(env)
	log('=================== ' + ecf_target(target) + ' ===================')
	log_date()

	rc_copied_to_target = None

	if env['PLATFORM'] == 'win32':
		rc = os.path.splitext(str(source[0]))[0] + '.rc'

		if os.path.exists(rc):
			project_path = dirname(str(target[0]), 4)
			rc_copied_to_target = os.path.join(project_path, os.path.basename(rc))

			if rc == rc_copied_to_target:
				rc_copied_to_target = None
			else:
				f = open(rc, 'r')
				try: s = f.read()
				finally: f.close()

				if s:
					icon_pattern = r'(\w+[ \t]+ICON[ \t]+[^"]*")([^"]+")'
					manifest_pattern = r'(CREATEPROCESS_MANIFEST_RESOURCE_ID[ \t]+RT_MANIFEST[ \t]+")([^"]+")'
					substitution = r'\g<1>' + os.path.dirname(rc).replace('\\', '/') + r'/\g<2>'
					s = re.sub(icon_pattern, substitution, s)
					s = re.sub(manifest_pattern, substitution, s)
					f = open(rc_copied_to_target, 'w')
					try: f.write(s)
					finally: f.close()

	flags = env['ECFLAGS'].split()
	if not '-target' in flags: flags += ['-target', ecf_target(target)]
	log_process([env['EC'], '-batch', '-config', str(source[0])] + flags)

	for t in target:
		if result == 0 and not os.path.exists(str(t)):
			print (log_file_tail())
			result = 1

	if rc_copied_to_target: os.remove(rc_copied_to_target)
	if log_file != sys.stdout: log_file.close()
	return result

def ec_emitter(target, source, env):
	"""
	The Eiffel Builder's emitter function.
	Parameters:
	 * target[0]: the ECF target to be built; if empty then defaults to the first target in the ECF file.
	 * source[0]: the ECF file. The paths to the files to be built are computed by reading this file.
	 * source[1], source[2], etc.: additional optional dependencies (precompiled libraries, ".rc" files, etc.).
	 * env['ECFLAGS']: Eiffel compiler flags. The paths to the files to be built are affected by these.
	Result emits the target and source parameters passed to ec_action().
	 * The source emitted is exactly the same as the source parameter passed in.
	 * The target emitted contains one or more calculated file paths.
	   Each target file is in the directory {-project_path}/EIFGENs/{-target}/{-finalize}, where:
		-project_path if omitted defaults to the current working directory;
		-target if omitted defaults to the base name of target[0] (or else to the first target in the ECF file);
		-finalize evaluates to "F_code", else if omitted defaults to "W_code".
	   The number of target file paths emitted, and the actual file names, depend on several factors:
	    * Options specified inside the ECF file;
	    * The -precompile flag;
	    * The -c_compile flag.
	"""
	result = None

	if len(target) > 0:
		ec_target = os.path.basename(str(target[0]))
	else:
		ec_target = ""

	"""
	elif not env.Detect(env['EC']):
		print '****** ERROR! The Eiffel compiler ' + env['EC'] + ' is missing from your path: cannot build ' + ec_target
	"""
	if len(source) == 0:
		print ('****** ERROR! No source .ecf file specified: cannot build ' + ec_target)
	else:
		ecf = str(source[0])
		ec_path = os.getcwd()
		ec_code = 'W_code'
		exe_name = dotnet_type = is_dotnet = is_precompiling = is_c_compiling = is_shared_library = None

		flags = env['ECFLAGS'].split()

		for i, flag in enumerate(flags):
			if flag == '-project_path':
				ec_path = flags[i + 1]
			elif flag == '-target':
				ec_target = flags[i + 1]
			elif flag == '-finalize':
				ec_code = 'F_code'
			elif flag == '-precompile':
				is_precompiling = True
			elif flag == '-c_compile':
				is_c_compiling = True

		ecf_as_xml = xml.dom.minidom.parse(ecf)
		ec_target_next = ec_target

		while ec_target_next != None:
			t = ec_target_next
			ec_target_next = None

			for element in ecf_as_xml.getElementsByTagName('target'):
				name = element.attributes['name'].value
				if ec_target == "": t = ec_target = name

				if t == name:
					if element.hasAttribute('extends'):
						ec_target_next = element.attributes['extends'].value

					for setting in element.getElementsByTagName('setting'):
						name = setting.attributes['name'].value

						if name == 'msil_generation':
							if is_dotnet == None:
								is_dotnet = setting.attributes['value'].value == 'true'
						elif name == 'msil_generation_type':
							if dotnet_type == None:
								dotnet_type = '.' + setting.attributes['value'].value
						elif name == 'executable_name':
							if exe_name == None:
								exe_name = setting.attributes['value'].value
						elif name == 'shared_library_definition':
							if is_shared_library == None:
								is_shared_library = True

		if exe_name == None:
			exe_name = str(ecf_as_xml.documentElement.attributes['name'].value)

		if dotnet_type:
			ext = dotnet_type
		elif is_precompiling:
			ext = '.melted'
		elif is_shared_library:
			exe_name = env['SHLIBPREFIX'] + exe_name
			ext = env['SHLIBSUFFIX']
		else:
			exe_name = env['PROGPREFIX'] + exe_name
			ext = env['PROGSUFFIX']

		ec_path += '/EIFGENs/' + ec_target + '/'

		if is_c_compiling:
			ec_path += ec_code + '/'
			result = [ec_path + exe_name + ext]

			if is_dotnet:
				result += [ec_path + 'lib' + exe_name + '.dll']
			elif is_precompiling:
				result += [ec_path + environment_variable(env, 'ISE_C_COMPILER') + '/' + env['PROGPREFIX'] + 'driver' + env['PROGSUFFIX']]
			elif is_shared_library and env['PLATFORM'] == 'win32':
				result += [ec_path + 'dll_' + exe_name + '.lib']
		else:
			result = [ec_path + ec_code + '/Makefile.SH', ec_path + 'project.epr']

	return result, source

ecf_environment_variable_regex = re.compile(r'(\$\||\$\(?\w*\)?|[^$]+)', re.M)

def ecf_scanner(node, env, path):
	"""
	All dependencies mentioned in 'node', which is expected to be an ECF file.
	The dependencies consist of:
	 * All Eiffel class files found in all clusters (including override clusters) mentioned in the ECF file.
	 * All .ecf library files mentioned in the ECF file. (Such libraries are not themselves scanned).
	 * All .NET assemblies mentioned in the ECF file.
	 * All external object files mentioned in the ECF file.
	 * All .h and .hpp files found in external include directories mentioned in the ECF file.
	Because this ignores targets and conditionals in the ECF file, it may cause unnecessary builds.
	"""

	def element_location(element):
		"""
		The 'location' attribute of 'element', processed to take care of:
		 * Expansion of environment variables.
		 * If 'location' is relative, prefixing with the directory name of 'node'.
		 * If 'location' is a nested cluster, prefixing with the location of the parent element (recursively).
		"""
		result = ''
		ecf_config_path = os.path.dirname(os.path.abspath(str(node)))

		for token in ecf_environment_variable_regex.findall(element.attributes['location'].value):
			if token[0] != r'$':
				result += token
			elif token == r'$|':
				result += element_location(element.parentNode) + '/'
			elif token == r'$ECF_CONFIG_PATH' or token == r'$(ECF_CONFIG_PATH)':
				result += ecf_config_path
			else:
				s = environment_variable(env, token)

				if s:
					result += s
				else:
					print ('****** WARNING!', str(node), 'uses undefined environment variable', token)

		result = result.replace('\\', '/')

		if not os.path.isabs(result):
			result = os.path.join(ecf_config_path, result)

		return result

	result = []
	ecf_as_xml = xml.dom.minidom.parse(str(node))

	for tag in ['cluster', 'override', 'tests', 'library', 'assembly', 'external_include', 'external_object']:
		for element in ecf_as_xml.getElementsByTagName(tag):
			location = element_location(element)

			if os.path.isfile(location):
				result += [location]
			elif tag == 'external_include':
				result += env.Glob(location + '/*.h') + env.Glob(location + '/*.hpp')
			elif element.attributes.get('recursive', None):
				result += classes_in_cluster(env, location)
			else:
				result += env.Glob(location + '/*.e')

	return result

def ecf_target(target, source = None, env = None):
	"""The ECF target corresponding to the given build target."""
	return os.path.basename(dirname(str(target[0]), 2))

def generate(env):
	"""Add a Builder and construction variables for Eiffel to the given Environment."""
	default_ec_path = env.WhereIs('ec')

	if not default_ec_path:
		default_ec_path = spec_path(env, 'studio', 'bin/ec')
		if default_ec_path == '': default_ec_path = 'ec'

	vars = Variables()
	vars.Add('EC', "The Eiffel command-line compiler.", default_ec_path)
	vars.Add('ECFLAGS', "Use ec -help to see possible options.", '-finalize -clean -c_compile')
	vars.Add('ECLOG', "File to log Eiffel compiler output.", 'SCons.Eiffel.log')
	vars.Update(env)
	Help(vars.GenerateHelpText(env))

	env['BUILDERS']['Eiffel'] = Builder(action = Action(ec_action, ecf_target), emitter = ec_emitter, target_factory = Entry)
	env.Append(SCANNERS = Scanner(ecf_scanner, skeys = ['.ecf']))
	env.AddMethod(environment_variable, "EiffelEnvironmentVariable")
	env.AddMethod(classes_in_cluster, "EiffelClassesInCluster")
	env.AddMethod(spec_path, "EiffelSpecPath")

	for v in ['ISE_EIFFEL', 'ISE_PLATFORM', 'ISE_C_COMPILER']:
		if not environment_variable(env, v):
			print ('****** WARNING! Undefined Eiffel environment variable ' + v + '.')

def exists(env):
	"""Is the Eiffel compiler available?"""
	return env.Detect(env['EC'])

# Utility functions.

def environment_variable(env, var):
	"""
	The value of the environment variable 'var' within 'env'.
	If undefined and it is one of the standard EiffelStudio variables, a sensible platform-specific assumption is used; else None.
	"""
	result = None
	var = var.lstrip('$').lstrip('(').rstrip(')')

	if var in env['ENV']:
		result = env['ENV'][var]
	elif var == 'ISE_PLATFORM':
		if env['PLATFORM'] == 'win32':
			result = 'windows'
		elif env['PLATFORM'] == 'darwin':
			result = 'macosx-x86'
		else:
			result = 'linux-x86'
	elif var == 'ISE_C_COMPILER':
		if env['PLATFORM'] == 'win32':
			result = 'mingw'
		else:
			result = 'gcc'
	elif var == 'ISE_EIFFEL':
		if 'EC' in env: result = env.WhereIs(env['EC'])
		if result: result = os.path.abspath(dirname(result, 5))
	elif var == 'ISE_LIBRARY':
		result = environment_variable(env, 'ISE_EIFFEL')

	return result

def classes_in_cluster(env, cluster):
	"""All Eiffel class files in the given cluster and its subclusters."""
	result = []

	for root, dirnames, filenames in os.walk(cluster):
		if '.svn' in dirnames: dirnames.remove('.svn')
		if '.git' in dirnames: dirnames.remove('.git')
		result += env.Glob(root + '/*.e')

	return result

def spec_path(env, mid_part, tail):
	"""
	A platform-dependent path in the EiffelStudio installation directory of the form:
	$ISE_EIFFEL + mid_part + '/spec/' + $ISE_PLATFORM + '/' + tail
	If either of these environment variables is undefined, then the result is an empty string.
	"""
	result = ''
	ise_eiffel = environment_variable(env, 'ISE_EIFFEL')
	ise_platform = environment_variable(env, 'ISE_PLATFORM')

	if ise_eiffel and ise_platform:
		result = os.path.join(ise_eiffel, mid_part)
		result = os.path.join(result, 'spec')
		result = os.path.join(result, ise_platform)
		result = os.path.join(result, tail)
		result = os.path.abspath(result)

	return result

def dirname(path, n):
	"""The directory name of 'path', called recursively 'n' times."""
	result = path
	if n > 0: result = dirname(os.path.dirname(path), n - 1)
	return result
